/*!
 * Cumulus - 3D Tag Clouds
 * 
 * @version 0.4a
 * @license MIT Style
 * @author firejune <to@firejune.com>
 * @url http://firejune.com/1464
 * @requires prototype 1.6.0+ <www.prototypejs.org>
 *
 */

var Cumulus = Class.create({
  initialize: function(reference, options) {
    this.options = Object.extend({
      fontFamily: Prototype.Browser.IE ? 'Tahoma' : "",
      hoverEffect: true, 
      horizontalAngle: 0.1,
      spinSpeed: 0.0015,
      cameraFocus: 0
    }, options); 

    (reference = this.reference = $(reference)).setStyle({
      position: 'relative',
      zIndex: '10000',
      display: 'block',
      fontFamily: this.options.fontFamily
    }).observe('mousemove', this.onMouseMove.bindAsEventListener(this));

    this.mouseX = 5;
    this.mouseY = 5;
    this.distanceX = 0;
    this.distanceY = 0;
    this.centerX = reference.getWidth() >> 1;
    this.centerY = reference.getHeight() >> 1;

    this.matrix = new Matrix3D();
    this.camera = new Camera3D();
    this.camera.focus = this.options.cameraFocus || this.centerX * 2;
    this.focuszoom = this.camera.focus * this.camera.zoom;
    this.transform = this.getTransformProperty(reference);

    var elements = this.elements = reference.childElements();
    var radius = this.centerX / 1.2;   
    for (i = 0; i < elements.length; i++) {
      var phi = Math.acos(-1 + (2 * i) / elements.length);
      var theta = Math.sqrt(elements.length * Math.PI) * phi;
      var fixed = elements[i].className.match('fixed');
      Object.extend(elements[i].setStyle({
        position: 'absolute',
        width: elements[i].offsetWidth + 'px'
      }), {
        x: fixed ? 0 : radius * Math.cos(theta) * Math.sin(phi),
        y: fixed ? 0 : radius * Math.sin(theta) * Math.sin(phi),
        z: fixed ? 0 : radius * Math.cos(phi),
        w: elements[i].offsetWidth >> 1,
        h: elements[i].offsetHeight >> 1
      });
    }

    // Javascript to append CSS
    if (this.options.hoverEffect) {
      if (!reference.id) reference.id = '__cumulus__';
      var css = [
        '#' + reference.id + ' > * {padding: 2px 7px; background: transparent; -moz-border-radius: 5px; -webkit-border-radius: 5px;}',
        '#' + reference.id + ' > *:hover {border: 2px solid #aaa; background: #eee;}'
      ].join('');
      // this is required in CSS spec!
      var style = new Element('style', {type: 'text/css', media: 'screen, projection'});
      // must insert into DOM before setting css text
      document.getElementsByTagName('head')[0].appendChild(style);
      if (style.styleSheet) style.styleSheet.cssText = css; // IE
      else style.appendChild(document.createTextNode(css)); // w3c
    }

    this.getOffset();
    this.renderFrame();

    Event.observe(window, 'resize', this.getOffset.bind(this));
    this.play();
  },

  play: function() {
    this.interval = setInterval(this.renderFrame.bind(this), 20);
  },

  stop: function() {
    this.interval && clearInterval(this.interval);
  },

  getTransformProperty: function(element) {
    var properties = ['transform', 'WebkitTransform', 'MozTransform'], p;
    while (p = properties.shift()) if (typeof element.style[p] != 'undefined') return p;
    return false;
  },

  getOffset: function() {
    var offset = this.reference.cumulativeOffset();
    this.offsetX = offset.left;
    this.offsetY = offset.top;
  },

  onMouseMove: function(event) {
    this.mouseX = (event.pointerX() - this.offsetX - this.centerX) * 0.1;
    this.mouseY = (event.pointerY() - this.offsetY - this.centerY) * 0.1;
  },

  renderFrame: function() {
    this.distanceX += this.mouseX * this.options.spinSpeed;
    this.distanceY += (this.mouseY - this.distanceY) * this.options.horizontalAngle;
    this.camera.x = Math.sin(this.distanceX) * (this.camera.focus * 2);
    this.camera.y = -this.distanceY * 10;
    this.camera.z = Math.cos(this.distanceX) * (this.camera.focus * 2);

    this.matrix.lookAt(this.camera, this.camera.target, this.camera.up);

    var i = this.elements.length;
    while(--i >= 0) {
      var element = this.elements[i];
      if (element.fixed) continue;
      element.sz = element.x * this.matrix.n31 + element.y * this.matrix.n32 + element.z * this.matrix.n33 + this.matrix.n34;

      var scale = this.focuszoom / (this.camera.focus + element.sz);
      var opacity = (scale - 0.9) * 10 + 0.3;
      if (this.camera.focus + element.sz < 0 || opacity < 0) {
        element.style['visibility'] = 'hidden';
        continue;
      }

      element.sx = (element.x * this.matrix.n11 + element.y * this.matrix.n12 + element.z * this.matrix.n13 + this.matrix.n14) * scale;
      element.sy = (element.x * this.matrix.n21 + element.y * this.matrix.n22 + element.z * this.matrix.n23 + this.matrix.n24) * -scale;
      element.style.visibility = 'visible';
      element.style.zIndex = 1000 + (-element.sz * 100) >> 0;
      element.style.left = (element.sx + this.centerX - element.w) + 'px';
      element.style.top = (element.sy + this.centerY - element.h) + 'px';
      element.setOpacity(opacity);
      if (this.transform) element.style[this.transform] = 'scale(' + scale + ')';
      if (!element.x) element.fixed = true;
    }
  }
});


/**
 * 3D Engine
 */

/* Vector3D.js */
var Vector3D = Class.create({
  x: null, y: null, z: null, sx: null, sy: null, sz: null, userData: null,
  dx: null, dy: null, dz: null, tx: null, ty: null, tz: null, oll: null,
  initialize: function(a, b, c) {
    this.x = a;
    this.y = b;
    this.z = c;
  },
  copy: function(v) {
    this.x = v.x;
    this.y = v.y;
    this.z = v.z;
  },
  add: function(v) {
    this.x += v.x;
    this.y += v.y;
    this.z += v.z;
  },
  sub: function(v) {
    this.x -= v.x;
    this.y -= v.y;
    this.z -= v.z;
  },
  cross: function(v) {
    this.tx = this.x;
    this.ty = this.y;
    this.tz = this.z;
    this.x = this.ty * v.z - this.tz * v.y;
    this.y = this.tz * v.x - this.tx * v.z;
    this.z = this.tx * v.y - this.ty * v.x;
  },
  multiply: function(s) {
    this.x *= s;
    this.y *= s;
    this.z *= s;
  },
  distanceTo: function(v) {
    this.dx = this.x - v.x;
    this.dy = this.y - v.y;
    this.dz = this.z - v.z;
    return Math.sqrt(this.dx * this.dx + this.dy * this.dy + this.dz * this.dz);
  },
  distanceToSquared: function(v) {
    this.dx = this.x - v.x;
    this.dy = this.y - v.y;
    this.dz = this.z - v.z;
    return this.dx * this.dx + this.dy * this.dy + this.dz * this.dz;
  },
  length: function() {
    return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
  },
  lengthSq: function() {
    return this.x * this.x + this.y * this.y + this.z * this.z;
  },
  negate: function() {
    this.x = -this.x;
    this.y = -this.y;
    this.z = -this.z;
  },
  normalize: function() {
    if (this.length() > 0) this.ool = 1.0 / this.length();
    else this.ool = 0;
    this.x *= this.ool;
    this.y *= this.ool;
    this.z *= this.ool;
    return this;
  },
  dot: function(v) {
    return this.x * v.x + this.y * v.y + this.z * v.z;
  },
  clone: function() {
    return new Vector3D(this.x, this.y, this.z);
  },
  toString: function() {
    return '(' + this.x + ', ' + this.y + ', ' + this.z + ')';
  }
});

Object.extend(Vector3D, {
  add: function(a, b) {
    return new Vector3D(a.x + b.x, a.y + b.y, a.z + b.z);
  },
  sub: function(a, b) {
    return new Vector3D(a.x - b.x, a.y - b.y, a.z - b.z);
  },
  multiply: function(a, s) {
    return new Vector3D(a.x * s, a.y * s, a.z * s);
  },
  cross: function(a, b) {
    return new Vector3D(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
  },
  dot: function(a, b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
  }
});

/* Matrix3D.js */
var Matrix3D = Class.create({
  n11: null, n12: null, n13: null, n14: null,
  n21: null, n22: null, n23: null, n24: null,
  n31: null, n32: null, n33: null, n34: null,
  x: new Vector3D(0,0,0),
  y: new Vector3D(0,0,0),
  z: new Vector3D(0,0,0),
  initialize: function() {
    this.identity();
  },
  identity: function() {
    this.n11 = 1;
    this.n12 = 0;
    this.n13 = 0;
    this.n14 = 0;
    this.n21 = 0;
    this.n22 = 1;
    this.n23 = 0;
    this.n24 = 0;
    this.n31 = 0;
    this.n32 = 0;
    this.n33 = 1;
    this.n34 = 0;
    this.x = new Vector3D(0,0,0);
    this.y = new Vector3D(0,0,0);
    this.z = new Vector3D(0,0,0);
  },
  lookAt: function(eye, center, up) {
    this.z.copy(center);
    this.z.sub(eye);
    this.z.normalize();
    this.x.copy(this.z);
    this.x.cross(up);
    this.x.normalize();
    this.y.copy(this.x);
    this.y.cross(this.z);
    this.y.normalize();
    this.n11 = this.x.x;
    this.n12 = this.x.y;
    this.n13 = this.x.z;
    this.n14 = -this.x.dot(eye);
    this.n21 = this.y.x;
    this.n22 = this.y.y;
    this.n23 = this.y.z;
    this.n24 = -this.y.dot(eye);
    this.n31 = this.z.x;
    this.n32 = this.z.y;
    this.n33 = this.z.z;
    this.n34 = -this.z.dot(eye);
  },
  transform: function(v) {
    var vx = v.x, vy = v.y, vz = v.z;
    v.x = this.n11 * vx + this.n12 * vy + this.n13 * vz + this.n14;
    v.y = this.n21 * vx + this.n22 * vy + this.n23 * vz + this.n24;
    v.z = this.n31 * vx + this.n32 * vy + this.n33 * vz + this.n34;
  },
  transformVector: function(v) {
    var vx = v.x, vy = v.y, vz = v.z;
    v.x = this.n11 * vx + this.n12 * vy + this.n13 * vz;
    v.y = this.n21 * vx + this.n22 * vy + this.n23 * vz;
    v.z = this.n31 * vx + this.n32 * vy + this.n33 * vz;
  },
  multiply: function(a, b) {
    var a11 = a.n11;
    var b11 = b.n11;
    var a21 = a.n21;
    var b21 = b.n21;
    var a31 = a.n31;
    var b31 = b.n31;
    var a12 = a.n12;
    var b12 = b.n12;
    var a22 = a.n22;
    var b22 = b.n22;
    var a32 = a.n32;
    var b32 = b.n32;
    var a13 = a.n13;
    var b13 = b.n13;
    var a23 = a.n23;
    var b23 = b.n23;
    var a33 = a.n33;
    var b33 = b.n33;
    var a14 = a.n14;
    var b14 = b.n14;
    var a24 = a.n24;
    var b24 = b.n24;
    var a34 = a.n34;
    var b34 = b.n34;
    this.n11 = a11 * b11 + a12 * b21 + a13 * b31;
    this.n12 = a11 * b12 + a12 * b22 + a13 * b32;
    this.n13 = a11 * b13 + a12 * b23 + a13 * b33;
    this.n14 = a11 * b14 + a12 * b24 + a13 * b34 + a14;
    this.n21 = a21 * b11 + a22 * b21 + a23 * b31;
    this.n22 = a21 * b12 + a22 * b22 + a23 * b32;
    this.n23 = a21 * b13 + a22 * b23 + a23 * b33;
    this.n24 = a21 * b14 + a22 * b24 + a23 * b34 + a24;
    this.n31 = a31 * b11 + a32 * b21 + a33 * b31;
    this.n32 = a31 * b12 + a32 * b22 + a33 * b32;
    this.n33 = a31 * b13 + a32 * b23 + a33 * b33;
    this.n34 = a31 * b14 + a32 * b24 + a33 * b34 + a34;
  },
  toString: function() {
    return "11: " + this.n11 + ", 12: " + this.n12 + ", 13: " + this.n13 + ", 14: " + this.n14 + "\n" + "21: " + this.n21 + ", 22: " + this.n22 + ", 23: " + this.n23 + ", 24: " + this.n24 + "\n" + "31: " + this.n31 + ", 32: " + this.n32 + ", 33: " + this.n33 + ", 34: " + this.n34;
  }
});

Object.extend(Matrix3D, {
  translationMatrix: function(x, y, z) {
    var m = new Matrix3D();
    m.n14 = x;
    m.n24 = y;
    m.n34 = z;
    return m;
  },
  scaleMatrix: function(x, y, z) {
    var m = new Matrix3D();
    m.n11 = x;
    m.n22 = y;
    m.n33 = z;
    return m;
  },
  // Apply Rotation about X to Matrix
  rotationXMatrix: function(theta) {
    var rot = new Matrix3D();
    rot.n22 = rot.n33 = Math.cos(theta);
    rot.n32 = Math.sin(theta);
    rot.n23 = -rot.n32;
    return rot;
  },
  // Apply Rotation about Y to Matrix
  rotationYMatrix: function(theta) {
    var rot = new Matrix3D();
    rot.n11 = rot.n33 = Math.cos(theta);
    rot.n13 = Math.sin(theta);
    rot.n31 = -rot.n13;
    return rot;
  },
  // Apply Rotation about Z to Matrix
  rotationZMatrix: function(theta) {
    var rot = new Matrix3D();
    rot.n11 = rot.n22 = Math.cos(theta);
    rot.n21 = Math.sin(theta);
    rot.n12 = -rot.n21;
    return rot;
  },
  rotationMatrix: function(ry, rx, rz) {
    var sx, sy, sz;
    var cx, cy, cz;
    sx = Math.sin(rx);
    sy = Math.sin(ry);
    sz = Math.sin(rz);
    cx = Math.cos(rx);
    cy = Math.cos(ry);
    cz = Math.cos(rz);
    var rot = new Matrix3D();
    rot.n11 = cx * cz - sx * sy * sz;
    rot.n12 = -cy * sz;
    rot.n13 = sx * cz + cx * sy * sz;
    rot.n21 = cx * sz + sx * sy * cz;
    rot.n22 = cy * cz;
    rot.n23 = sx * sz - cx * sy * cz;
    rot.n31 = -sx * cy;
    rot.n32 = sy;
    rot.n33 = cx * cy;
    return rot;
  }
});

/* Camera3D.js */
var Camera3D = Class.create(Vector3D.prototype, {
  up: null, target: null, zoom: null, focus: null, roll: null,
  initialize: function() {
    this.up = new Vector3D(0,1,0);
    this.target = new Vector3D(0,0,0);
    this.zoom = 3;
    this.focus = 500;
    this.roll = 0;
  }
});